const {Op,DatabaseError} = require('sequelize');
const sequelize = require('./db');
const {Fail,Success} = require("./HttpException");

//TODO 批量创建 批量更新
//  :id这样带动态参数的路由 和 靠[delete]、[put]方法来区分具有同一路由名的路由 在权限校验时不好校验
//  最好这样命名路由 article/add、article/update、article/delete .... (名字是唯一的)
const registerCRUDRoute = (
  {XValidator, XModel, router}
  ,include=['create','update','delete','bulkDelete','getList','get']) => {

  const opt = {}; // ←要注册哪些路由 以及 注册该条路由时需要用到的配置项
  include.forEach(item => {
    const x = item.split('.');
    opt[x[0]] = [...x.slice(1)];
  });
  // console.log('opt:', opt); // opt: { create: [ 'isAssociational:ArticleTag' ], update: [ 'isAssociational:ArticleTag', 'isOwn' ] , delete: [], bulkDelete: [], getList: [], get: [] }

  /** 注册创建资源路由*/
  if (opt.create) {
    let which_scene = 'post';
    opt.create.includes('isUser') && (which_scene = 'userCreate');

    const associate_model_name_arr = [];
    let isAssociational = false;
    for(let item of opt.create){
      if (item.startsWith('isAssociational')) {
        isAssociational = true;
        associate_model_name_arr.push(...item.split(':')[1].split(','));
        break;
      }
    }
    // console.log('association_models_arr:',association_models_arr); // association_models_arr: [ 'ArticleTag' ]

    router.post('/', async (ctx, next) => {
      const v = await new XValidator().scene(which_scene).validate(ctx);

      // post验证场景中的所有字段都会作为预备要创建的字段数据
      const createObj = {};
      v.$scene['post'].forEach(key => {
        createObj[key] = v.get(`body.${key}`);
      });
      if(ctx.path.startsWith('/api/user')) createObj.UserId = v.get('userid');

      // deal association
      const toBeAssociated = [];
      const include = [];
      let t;
      if(isAssociational){
        for(let model_name of associate_model_name_arr){
          const associate_field_value = v.get(`body.${model_name}`);
          if(!associate_field_value) break;

          if (Array.isArray(associate_field_value) && associate_field_value.length !== 0) {
            const x = associate_field_value[0];

            // 情景一: 添加了一篇文章 且 需要关联多个已有标签
            // ↓ article obj
            /*{
              // ... some article model own attributes
              ArticleTags: [[1,2,3]]
            }*/
            if (typeof x === 'number') {
              //setBars(接受一个id数组)
              toBeAssociated.push([model_name, associate_field_value]);
              break;
            }

            // 情景二: 新建了一个产品 附带需要创建多个标签
            // TODO 无法处理 要创建的标签中已经有了的情况 (并且这种情况会影响foreignKey值的设置
            /*{
              // ... some product model own attributes
              tags: [
                {name: 'Alpha'},
                {name: 'Beta'}
              ]
            } */
            if (x.__proto__ === Object.prototype) {
              include.push(require('../app/model')[model_name]);
              break;
            }
          }

          // 情景三：新建了一个用户 附带将用户附加信息写入用户详情表
          // ↓ user obj
          /*{
            // ... some user model self attribute
            UserProfile:{
              job:'xxx'
              ,age: 11
            }
          }*/
          if(associate_field_value.__proto__ === Object.prototype){
            include.push({association: XModel[model_name]});
            break;
          }
        }
        t = await sequelize.transaction()
      }

      try {
        const createdItem = await XModel.create(createObj, {include}, t ? {transaction: t} : {});
        if (toBeAssociated.length !== 0) {
          const promiseAll_arr = [];
          for(let [model_name,idsArr] of toBeAssociated){
            promiseAll_arr.push(createdItem[`set${require('../app/model')[model_name].options.name.plural}`](idsArr, t ? {transaction: t} : {}))
          }
          await Promise.all(promiseAll_arr);
        }
        t && await t.commit();
        helper.success(createdItem);
      } catch (e) {
        if (e.name === 'SequelizeForeignKeyConstraintError') throw new helper.ParameterException({msg: `${e.fields[0]}关联的记录不存在!`}); // 如果是SequelizeForeignKeyConstraintError错误 就会有一个fields属性 里面存放着这个foreignKey
        if (!(e instanceof Success) && !(e instanceof Fail)) t && await t.rollback();
        throw e;
      }
    });
  }

  /** 注册更新资源路由*/
  if (opt.update) {
    let which_scene = 'put';
    opt.update.includes('isOwn') && (which_scene = 'putOwn');

    const associate_model_name_arr = [];
    let isAssociational = false;
    for(let item of opt.update){
      if (item.startsWith('isAssociational')) {
        isAssociational = true;
        associate_model_name_arr.push(...item.split(':')[1].split(','));
        break;
      }
    }

    router.put('/:id', async (ctx, next) => {
      const v = await new XValidator().scene(which_scene).validate(ctx);

      const updateObj = {}; // 如果传了多余的字段(模型定义中不存在的字段) 是会被自动剔除的(生成sql语句时)
      v.$scene['put'].forEach(key => {
        updateObj[key] = v.get(`body.${key}`); // 注意这里如果有validateRecordIfExist这样的也没有关系 生成sql语句时会自动剔除掉
      });
      if(ctx.path.startsWith('/api/user')) updateObj.UserId = v.get('userid');

      // deal association
      const toBeAssociated = [];
      const tobeUpdated = [];
      let t;
      if(isAssociational){
        for(let model_name of associate_model_name_arr){
          const associate_field_value = v.get(`body.${model_name}`);
          if(!associate_field_value) break;

          if (Array.isArray(associate_field_value) && associate_field_value.length !== 0) {
            const x = associate_field_value[0];

            // 情景一: 修改一篇文章 需要修改关联的多个已有标签
            // ↓ article obj
            /*{
              // ... some article model own attributes
              ArticleTags: [[1,2,3]]
            }*/
            if (typeof x === 'number') {
              //setBars(接受一个id数组)
              toBeAssociated.push([model_name, associate_field_value]);
              break;
            }
          }

          // 情景二：更新一个用户 附带将更新的用户附加信息写入用户详情表
          // ↓ user obj
          /*{
            // ... some user model self attribute
            UserProfile:{
              job:'xxx'
              ,age: 11
            }
          }*/
          if(associate_field_value.__proto__ === Object.prototype){
            tobeUpdated.push([model_name, associate_field_value]);
            break;
          }
        }
        t = await sequelize.transaction()
      }

      try {
        const opt = Object.assign({}, {where: {id: ctx.params.id}}, t ? {transaction: t} : {});
        const affectedRows = await XModel.update(updateObj, opt);
        const promiseAll_arr = [];

        // 一般是 更新一篇文章 同时需要更新它有的所有的标签 这样的情景
        if (toBeAssociated.length !== 0) {
          const cur = await XModel.findOne({id: ctx.params.id});
          for(let [model_name,idsArr] of toBeAssociated){
            promiseAll_arr.push(cur[`set${require('../app/model')[model_name].options.name.plural}`](idsArr, t ? {transaction: t} : {}));
          }
        }

        // 一般是 更新用户 同时需要更新他的详情表 这样的情景
        if (tobeUpdated.length !== 0) {
          for (let [model_name,updateObj] of tobeUpdated){
            const opt = Object.assign({}, {where: {[`${XModel}Id`]: ctx.params.id}}, t ? {transaction: t} : {});
            promiseAll_arr.push(require('../app/model')[model_name].update(updateObj, opt));
          }
        }
        await Promise.all(promiseAll_arr);
        t && await t.commit();
        helper.success({affectedRows});
      } catch (e) {
        if (e.name === 'SequelizeForeignKeyConstraintError') throw new helper.ParameterException({msg: `${e.fields[0]}关联的记录不存在!`});
        if (!(e instanceof Success) && !(e instanceof Fail)) t && await t.rollback();
        throw e;
      }
    });
  }

  /** 注册批量删除资源路由*/
  opt.bulkDelete && router.delete('/', async (ctx, next) => {
    // 考虑到查询性能,如果删除前还要验证每一条记录是否存在,会进行多次查询,很浪费性能,故我们直接删除,在数据库层面如果IN的值不存在,它是没有什么影响的
    // 一般不提供给前端用户, 前端用户想要批量删除, 可以多次调用单次删除
    const affectedRows = await XModel.destroy({
      where: {
        id: {
          [Op.in]: ctx.request.body.ids
        }
      }
    });
    helper.success({affectedRows});
  });

  /** 注册删除资源路由*/
  //TODO 注意 这里是软删除(db.js里paranoid:true), 这意味着如果软删除里的记录里存在着unique的字段, 因为是软删除, 这个字段仍然是占着这个值的, 新的数据(record)仍然不能在这个字段上填相同的值
  if(opt.delete){
    let which_scene = 'recordIfExist';
    opt.delete.includes('isOwn') && (which_scene = 'deleteOwn');

    router.delete('/:id', async (ctx, next) => {
      const v = await new XValidator().scene(which_scene).validate(ctx);
      const affectedRows = await XModel.destroy({
        where: {
          id: ctx.params.id
        }
      });
      helper.success({affectedRows});
    });
  }

  /** 注册获取资源列表路由*/
  opt.getList && router.get('/list', async (ctx, next) => {
    // let {page_number = 1, limit = config.page_size, ...whereObj} = ctx.request.query;

    /** emmm 不安全*/
    // for (let key in whereObj) {
    //   whereObj[key] = {
    //     [Op.like]: whereObj[key]
    //   }
    // }

    const v = await new XValidator().scene('list').validate(ctx);
    const {page_number = 1, limit = config.page_size} = v.get('query');
    const {count, rows} = await XModel.findAndCountAll({
      // where: whereObj,
      offset: (page_number - 1) * limit,
      limit: limit
    });
    helper.success({total: count, rows, page_number})
  });

  /** 注册获取资源路由*/
  //TODO 必须放在[get]/list后面 不然[get]/list会当做[get]/:id被匹配上
  opt.get && router.get('/:id', async (ctx, next) => {
    const v = await new XValidator().scene('recordIfExist').validate(ctx);
    const row = await XModel.findByPk(ctx.params.id, {
      attributes: {exclude: ['deletedAt']}
    });
    helper.success(row)
  });
};

module.exports = registerCRUDRoute;
